/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.crazymaker.servlet.container.netty.resource;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;

@Slf4j
public abstract class AbstractArchiveResource extends AbstractResource
{

    private final AbstractArchiveResourceSet archiveResourceSet;
    private final String baseUrl;
    private final JarEntry resource;
    private final String codeBaseUrl;
    private final String name;
    private boolean readCerts = false;
    private Certificate[] certificates;

    protected AbstractArchiveResource(AbstractArchiveResourceSet archiveResourceSet,
                                      String webAppPath, String baseUrl, JarEntry jarEntry, String codeBaseUrl)
    {
        super(archiveResourceSet.getRoot(), webAppPath);
        this.archiveResourceSet = archiveResourceSet;
        this.baseUrl = baseUrl;
        this.resource = jarEntry;
        this.codeBaseUrl = codeBaseUrl;

        String resourceName = resource.getName();
        if (resourceName.charAt(resourceName.length() - 1) == '/')
        {
            resourceName = resourceName.substring(0, resourceName.length() - 1);
        }
        String internalPath = archiveResourceSet.getInternalPath();
        if (internalPath.length() > 0 && resourceName.equals(
                internalPath.subSequence(1, internalPath.length())))
        {
            name = "";
        } else
        {
            int index = resourceName.lastIndexOf('/');
            if (index == -1)
            {
                name = resourceName;
            } else
            {
                name = resourceName.substring(index + 1);
            }
        }
    }

    protected AbstractArchiveResourceSet getArchiveResourceSet()
    {
        return archiveResourceSet;
    }

    protected final String getBase()
    {
        return archiveResourceSet.getBase();
    }

    protected final String getBaseUrl()
    {
        return baseUrl;
    }

    protected final JarEntry getResource()
    {
        return resource;
    }

    @Override
    public long getLastModified()
    {
        return resource.getTime();
    }

    @Override
    public boolean exists()
    {
        return true;
    }

    @Override
    public boolean isVirtual()
    {
        return false;
    }

    @Override
    public boolean isDirectory()
    {
        return resource.isDirectory();
    }

    @Override
    public boolean isFile()
    {
        return !resource.isDirectory();
    }

    @Override
    public boolean delete()
    {
        return false;
    }

    @Override
    public String getName()
    {
        return name;
    }

    @Override
    public long getContentLength()
    {
        if (isDirectory())
        {
            return -1;
        }
        return resource.getSize();
    }

    @Override
    public String getCanonicalPath()
    {
        return null;
    }

    @Override
    public boolean canRead()
    {
        return true;
    }

    @Override
    public long getCreation()
    {
        return resource.getTime();
    }

    @Override
    public URL getURL()
    {
        String url = baseUrl + resource.getName();
        try
        {
            return new URL(url);
        } catch (MalformedURLException e)
        {
            if (log.isDebugEnabled())
            {
                log.debug("fileResource.getUrlFail:" + url);
            }
            return null;
        }
    }

    @Override
    public URL getCodeBase()
    {
        try
        {
            return new URL(codeBaseUrl);
        } catch (MalformedURLException e)
        {
            if (log.isDebugEnabled())
            {
                log.debug("fileResource.getUrlFail:" + codeBaseUrl, e);
            }
            return null;
        }
    }

    @Override
    public final byte[] getContent()
    {
        long len = getContentLength();

        if (len > Integer.MAX_VALUE)
        {
            // Can't create an array that big
            throw new ArrayIndexOutOfBoundsException(
                    "abstractResource.getContentTooLarge:" + Long.valueOf(len));
        }

        if (len < 0)
        {
            // Content is not applicable here (e.g. is a directory)
            return null;
        }

        int size = (int) len;
        byte[] result = new byte[size];

        int pos = 0;
        try (JarInputStreamWrapper jisw = getJarInputStreamWrapper())
        {
            if (jisw == null)
            {
                // An error occurred, don't return corrupted content
                return null;
            }
            while (pos < size)
            {
                int n = jisw.read(result, pos, size - pos);
                if (n < 0)
                {
                    break;
                }
                pos += n;
            }
            // Once the stream has been read, read the certs
            certificates = jisw.getCertificates();
            readCerts = true;
        } catch (IOException ioe)
        {
            if (log.isDebugEnabled())
            {
                log.debug("abstractResource.getContentFail:" +
                        getWebappPath(), ioe);
            }
            // Don't return corrupted content
            return null;
        }

        return result;
    }


    @Override
    public Certificate[] getCertificates()
    {
        if (!readCerts)
        {
            // TODO - get content first
            throw new IllegalStateException();
        }
        return certificates;
    }

    @Override
    public Manifest getManifest()
    {
        return archiveResourceSet.getManifest();
    }

    @Override
    protected final InputStream doGetInputStream()
    {
        if (isDirectory())
        {
            return null;
        }
        return getJarInputStreamWrapper();
    }

    protected abstract JarInputStreamWrapper getJarInputStreamWrapper();

    /**
     * This wrapper assumes that the InputStream was created from a JarFile
     * obtained from a call to getArchiveResourceSet().openJarFile(). If this is
     * not the case then the usage counting in AbstractArchiveResourceSet will
     * break and the JarFile may be unexpectedly closed.
     */
    protected class JarInputStreamWrapper extends InputStream
    {

        private final JarEntry jarEntry;
        private final InputStream is;
        private final AtomicBoolean closed = new AtomicBoolean(false);


        public JarInputStreamWrapper(JarEntry jarEntry, InputStream is)
        {
            this.jarEntry = jarEntry;
            this.is = is;
        }


        @Override
        public int read() throws IOException
        {
            return is.read();
        }


        @Override
        public int read(byte[] b) throws IOException
        {
            return is.read(b);
        }


        @Override
        public int read(byte[] b, int off, int len) throws IOException
        {
            return is.read(b, off, len);
        }


        @Override
        public long skip(long n) throws IOException
        {
            return is.skip(n);
        }


        @Override
        public int available() throws IOException
        {
            return is.available();
        }


        @Override
        public void close() throws IOException
        {
            if (closed.compareAndSet(false, true))
            {
                // Must only call this once else the usage counting will break
                archiveResourceSet.closeJarFile();
            }
            is.close();
        }


        @Override
        public synchronized void mark(int readlimit)
        {
            is.mark(readlimit);
        }


        @Override
        public synchronized void reset() throws IOException
        {
            is.reset();
        }


        @Override
        public boolean markSupported()
        {
            return is.markSupported();
        }

        public Certificate[] getCertificates()
        {
            return jarEntry.getCertificates();
        }
    }
}
